/*
 * 易族智汇（北京）科技有限公司 版权所有。
 * 未经许可，您不得使用此文件。
 * 官方地址：www.javamall.com.cn
 */
package com.enation.app.javashop.framework.security.sign;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.enation.app.javashop.framework.JavashopConfig;
import com.enation.app.javashop.framework.exception.ServiceException;
import com.enation.app.javashop.framework.exception.SystemErrorCodeV1;
import com.enation.app.javashop.framework.util.StringUtil;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;

import static com.enation.app.javashop.framework.security.sign.SignUtil.checkSign;
import static com.enation.app.javashop.framework.security.sign.SignUtil.findField;

/**
 * 防篡改机制切面
 * 1、思路为：
 * 拦截BaseMapper的insert、update等方法，对字段进行签名
 * 拦截selectOne、selectById方法，对签名校验
 * 2、注意：
 * update方法，如果updateWrapper为空，则需要手动处理签名情况，不在本机制范围内
 * selectOne、selectById的单一数据读取时时签名校验，如果校验失败signResult字段为0
 * 列表数据不做签名校验，通过定时任务进行扫描校验，并改变signResult字段
 *
 * @author kingapex
 * @version 1.0
 * @data 2021/11/18 17:46
 **/


@Service
public class SignAspect {

    @Autowired
    private JavashopConfig javashopConfig;


    /**
     * 本地线程，用来跳过本切面对baseMapper方法调用的拦截
     */
    private static ThreadLocal<Boolean> localCall = new ThreadLocal();

    @Around("selectOne()")
    public Object aroundSelectOne(Object entity) throws Throwable {
        return aroundGet(entity);
    }

    /**
     * 拦截SelectById方法，进行验签
     *
     * @param entity
     * @return
     * @throws Throwable
     */
    @Around("selectById()")
    public Object aroundSelectById(Object entity) throws Throwable {
        return aroundGet(entity);
    }

    /**
     * 拦截get One,供aroundSelectById和aroundSelectOne 使用
     *
     * @param entity
     * @return
     */
    private Object aroundGet(Object entity) throws Throwable {

        if (entity == null) {
            return null;
        }
        Class<?> clz = entity.getClass();
        Safely safely = clz.getAnnotation(Safely.class);

        //要开启签名并且签名的对象才做签名逻辑
        if (safely != null) {
            Field signResultField = findField(clz, SignResultField.class);
            ReflectionUtils.makeAccessible(signResultField);
            //开启验签并且 签名比对出错将签名结果置0  否则将结果设置为1
            if (javashopConfig.isSign() && !SignUtil.checkSign(entity)) {
                signResultField.set(entity, 0);
            } else {
                signResultField.set(entity, 1);
            }

        }


        return entity;
    }

    /**
     * 拦截update方法，生成签名并更新
     * 注意此时批量更新被改为多次更新
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("update()")
    public Object aroundUpdate(ProceedingJoinPoint point) throws Throwable {
        if(!javashopConfig.isSign()){
            return point.proceed();
        }
        Object[] args = point.getArgs();

        //第一个参数是DO对象
        Object entity = args[0];

        //第二个参数是updateWrapper
        UpdateWrapper updateWrapper = (UpdateWrapper) args[1];

        //如果entity为空，说明是按条件更新，需要进行手动处理
        //可以使用SignUpdateChainWrapper
        if (entity == null) {
            return point.proceed();
        }

        Class<?> clz = entity.getClass();
        Safely safely = clz.getAnnotation(Safely.class);

        //要开启签名并且签名的对象才做签名逻辑
        if (safely != null) {

            //****************************************************
            //无法处理updateWrapper为空的情况，注意：需要手动处理签名
            //****************************************************
            if (updateWrapper == null) {
                return point.proceed();
            }
            //拦截的target是baseMapper
            BaseMapper baseMapper = (BaseMapper) point.getTarget();

            List entityList = baseMapper.selectList(updateWrapper);

            //声明是本切面调用，以便使用updateById时，不要再进入切面
            localCall.set(true);

            int result = 0;
            for (Object dbEntity : entityList) {
                boolean signResult = checkSign(dbEntity);
                if (!signResult) {
                    throw new ServiceException(SystemErrorCodeV1.NOT_SAFE, "不能更改此数据，数据已经被篡改");
                }
                //填充要修改的数据，也就是用客户端形成的entity中的不为空的数据填充到数据库形成的entity
                fillModifyField(entity, dbEntity);

                //生成签名，并写入到签名字段
                String sign = SignUtil.createSign(dbEntity);
                writeSignFiled(sign, dbEntity);
                //System.out.printf("更新，签名数据["+sign+"]：" + entity);
                baseMapper.updateById(dbEntity);
                result++;
            }

            localCall.remove();

            return result;
        } else {
            return point.proceed();
        }
    }

    /**
     * 拦截updateById方法
     * 生成签名，并更新到数据库字段
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("updateById()")
    public Object aroundUpdateById(ProceedingJoinPoint point) throws Throwable {

        if(!javashopConfig.isSign()){
            return point.proceed();
        }

        Boolean isLocalCall = localCall.get();
        //本切面调用， 不要再进入切面
        if (isLocalCall != null && isLocalCall) {
            return point.proceed();
        }

        Object[] args = point.getArgs();

        Object entity = args[0];
        Class<?> clz = entity.getClass();
        Safely safely = clz.getAnnotation(Safely.class);
        //要开启签名并且签名的对象才做签名逻辑
        if (javashopConfig.isSign() && safely != null) {
            //检查是否有签名的字段要更新，如果没有则直接执行更新操作
            boolean exist = SignUtil.signFieldExisted(entity);
            if (!exist) {
                return point.proceed();
            }
            //找到主键
            Field idField = findField(clz, TableId.class);

            if (idField == null) {
                throw new RuntimeException("生成数据签名错误，" + clz + "没有主键");
            }
            ReflectionUtils.makeAccessible(idField);
            Serializable idValue = (Serializable) ReflectionUtils.getField(idField, entity);

            BaseMapper baseMapper = (BaseMapper) point.getTarget();
            Object dbObj = baseMapper.selectById(idValue);

            //用数据库中的数据填充
            fillNullSignField(dbObj, entity);

            //生成签名
            String sign = SignUtil.createSign(entity);

            //写入签名字段
            writeSignFiled(sign, entity);
            //System.out.printf("更新byid，签名数据["+sign+"]：" + entity);

        }
        //调用原业务
        Object result = point.proceed();
        return result;
    }

    /**
     * 拦截insert方法
     * 生成签名并写入数据库字段
     *
     * @param entity
     * @return
     * @throws Throwable
     */
    @Around("insert()")
    public void aroundInsert(Object entity) throws Throwable {

        Class<?> clz = entity.getClass();
        Safely safely = clz.getAnnotation(Safely.class);

        // 要开启签名并且实体类有加密注解，则需要加密
        if (javashopConfig.isSign()&&safely != null  ) {

            String sign = SignUtil.createSign(entity);
            //写入签名字段
            writeSignFiled(sign, entity);
            //System.out.printf("insert 签名数据["+sign+"]：" + entity);
        }else if (safely != null){
            //写入签名结果为正确
            Field signResultField = findField(clz, SignResultField.class);
            ReflectionUtils.makeAccessible(signResultField);
            signResultField.set(entity, 1);
        }


    }


    /**
     * 向签名字段写入签名
     *
     * @param sign   要写入的签名
     * @param entity do对象
     * @throws IllegalAccessException
     */
    private void writeSignFiled(String sign, Object entity) throws IllegalAccessException {
        Class<?> clz = entity.getClass();

        //写入签名
        Field signField = findField(clz, Sign.class);
        ReflectionUtils.makeAccessible(signField);
        signField.set(entity, sign);

        //写入签名结果为正确
        Field signResultField = findField(clz, SignResultField.class);
        ReflectionUtils.makeAccessible(signResultField);
        signResultField.set(entity, 1);

    }


    /**
     * copy 目标对象（要入库的对象）中需要参与签名，但是没有值的字段
     *
     * @param source 源对象（含数据库值的）
     * @param target 目标对象（要更新到数据库的）
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    private void fillNullSignField(Object source, Object target) throws IllegalAccessException, InvocationTargetException {
        Class<?> clz = source.getClass();
        List<Field> sourceFieldList = Arrays.stream(clz.getDeclaredFields()).filter(field -> field.getAnnotation(SignField.class) != null).collect(Collectors.toList());
        for (Field field : sourceFieldList) {

            ReflectionUtils.makeAccessible(field);
            Object targetValue = ReflectionUtils.getField(field, target);

            //目标对象，需要签名，但为空，需要用数据库中的数据填充
            if (targetValue == null) {
                ReflectionUtils.setField(field, target, field.get(source));
            }

        }
    }

    /**
     * 将源中修改的字段值赋给目标对象
     *
     * @param source 源对象
     * @param target 目标对象
     */
    private void fillModifyField(Object source, Object target) {

        Class<?> clz = source.getClass();
        ReflectionUtils.doWithFields(clz, field -> {

            int modifiers = field.getModifiers();
            boolean isFinal = Modifier.isFinal(modifiers);
            if (!isFinal) {
                ReflectionUtils.makeAccessible(field);
                Object valueObj = field.get(source);
                //值不为空则为要修改的字段
                if (valueObj != null) {
                    ReflectionUtils.setField(field, target, field.get(source));
                }
            }
        });
    }


}
